/**
 * Copyright (c) 2008, Gaudenz Alder
 */
package com.mxgraph.swing;

import com.mxgraph.util.*;
import com.mxgraph.util.mxEventSource.mxIEventListener;
import com.mxgraph.view.mxGraphView;

import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * An outline view for a specific graph component.
 */
public class mxGraphOutline extends JComponent {

    private static final Logger log = Logger.getLogger(mxGraphOutline.class.getName());

    /**
     *
     */
    private static final long serialVersionUID = -2521103946905154267L;

    /**
     * The constant DEFAULT_ZOOMHANDLE_FILL.
     */
    public static Color DEFAULT_ZOOMHANDLE_FILL = new Color(0, 255, 255);

    /**
     * The Graph component.
     */
    protected mxGraphComponent graphComponent;

    /**
     * TODO: Not yet implemented.
     */
    protected BufferedImage tripleBuffer;

    /**
     * Holds the graphics of the triple buffer.
     */
    protected Graphics2D tripleBufferGraphics;

    /**
     * True if the triple buffer needs a full repaint.
     */
    protected boolean repaintBuffer = false;

    /**
     * Clip of the triple buffer to be repainted.
     */
    protected mxRectangle repaintClip = null;

    /**
     * The Triple buffered.
     */
    protected boolean tripleBuffered = true;

    /**
     * The Finder bounds.
     */
    protected Rectangle finderBounds = new Rectangle();

    /**
     * The Zoom handle location.
     */
    protected Point zoomHandleLocation = null;

    /**
     * The Finder visible.
     */
    protected boolean finderVisible = true;

    /**
     * The Zoom handle visible.
     */
    protected boolean zoomHandleVisible = true;

    /**
     * The Use scaled instance.
     */
    protected boolean useScaledInstance = false;

    /**
     * The Anti alias.
     */
    protected boolean antiAlias = false;

    /**
     * The Draw labels.
     */
    protected boolean drawLabels = false;

    /**
     * Specifies if the outline should be zoomed to the page if the graph
     * component is in page layout mode. Default is true.
     */
    protected boolean fitPage = true;

    /**
     * Not yet implemented.
     * <p>
     * Border to add around the page bounds if wholePage is true.
     * Default is 4.
     */
    protected int outlineBorder = 10;

    /**
     * The Tracker.
     */
    protected MouseTracker tracker = new MouseTracker();

    /**
     * The Scale.
     */
    protected double scale = 1;

    /**
     * The Translate.
     */
    protected Point translate = new Point();

    /**
     * The Zoom gesture.
     */
    protected transient boolean zoomGesture = false;

    /**
     * The Repaint handler.
     */
    protected mxIEventListener repaintHandler = new mxIEventListener() {
        public void invoke(Object source, mxEventObject evt) {
            updateScaleAndTranslate();
            mxRectangle dirty = (mxRectangle) evt.getProperty("region");

            if (dirty != null) {
                repaintClip = new mxRectangle(dirty);
            } else {
                repaintBuffer = true;
            }

            if (dirty != null) {
                updateFinder(true);

                dirty.grow(1 / scale);

                dirty.setX(dirty.getX() * scale + translate.x);
                dirty.setY(dirty.getY() * scale + translate.y);
                dirty.setWidth(dirty.getWidth() * scale);
                dirty.setHeight(dirty.getHeight() * scale);

                repaint(dirty.getRectangle());
            } else {
                updateFinder(false);
                repaint();
            }
        }
    };

    /**
     * The Component handler.
     */
    protected ComponentListener componentHandler = new ComponentAdapter() {
        public void componentResized(ComponentEvent e) {
            if (updateScaleAndTranslate()) {
                repaintBuffer = true;
                updateFinder(false);
                repaint();
            } else {
                updateFinder(true);
            }
        }
    };

    /**
     * The Adjustment handler.
     */
    protected AdjustmentListener adjustmentHandler = new AdjustmentListener() {

        /**
         *
         */
        public void adjustmentValueChanged(AdjustmentEvent e) {
            if (updateScaleAndTranslate()) {
                repaintBuffer = true;
                updateFinder(false);
                repaint();
            } else {
                updateFinder(true);
            }
        }

    };

    /**
     * Instantiates a new Mx graph outline.
     *
     * @param graphComponent the graph component
     */
    public mxGraphOutline(mxGraphComponent graphComponent) {
        addComponentListener(componentHandler);
        addMouseMotionListener(tracker);
        addMouseListener(tracker);
        setGraphComponent(graphComponent);
        setEnabled(true);
        setOpaque(true);
    }

    /**
     * Is triple buffered boolean.
     *
     * @return the boolean
     */
    public boolean isTripleBuffered() {
        return tripleBuffered;
    }

    /**
     * Fires a property change event for <code>tripleBuffered</code>.
     *
     * @param tripleBuffered the tripleBuffered to set
     */
    public void setTripleBuffered(boolean tripleBuffered) {
        boolean oldValue = this.tripleBuffered;
        this.tripleBuffered = tripleBuffered;

        if (!tripleBuffered) {
            destroyTripleBuffer();
        }

        firePropertyChange("tripleBuffered", oldValue, tripleBuffered);
    }

    /**
     * Is draw labels boolean.
     *
     * @return the boolean
     */
    public boolean isDrawLabels() {
        return drawLabels;
    }

    /**
     * Fires a property change event for <code>drawLabels</code>.
     *
     * @param drawLabels the drawLabels to set
     */
    public void setDrawLabels(boolean drawLabels) {
        boolean oldValue = this.drawLabels;
        this.drawLabels = drawLabels;
        repaintTripleBuffer(null);

        firePropertyChange("drawLabels", oldValue, drawLabels);
    }

    /**
     * Is anti alias boolean.
     *
     * @return the antiAlias
     */
    public boolean isAntiAlias() {
        return antiAlias;
    }

    /**
     * Fires a property change event for <code>antiAlias</code>.
     *
     * @param antiAlias the antiAlias to set
     */
    public void setAntiAlias(boolean antiAlias) {
        boolean oldValue = this.antiAlias;
        this.antiAlias = antiAlias;
        repaintTripleBuffer(null);

        firePropertyChange("antiAlias", oldValue, antiAlias);
    }

    /**
     *
     */
    public void setVisible(boolean visible) {
        super.setVisible(visible);

        // Frees memory if the outline is hidden
        if (!visible) {
            destroyTripleBuffer();
        }
    }

    /**
     * Sets finder visible.
     *
     * @param visible the visible
     */
    public void setFinderVisible(boolean visible) {
        finderVisible = visible;
    }

    /**
     * Sets zoom handle visible.
     *
     * @param visible the visible
     */
    public void setZoomHandleVisible(boolean visible) {
        zoomHandleVisible = visible;
    }

    /**
     * Is fit page boolean.
     *
     * @return the boolean
     */
    public boolean isFitPage() {
        return fitPage;
    }

    /**
     * Fires a property change event for <code>fitPage</code>.
     *
     * @param fitPage the fitPage to set
     */
    public void setFitPage(boolean fitPage) {
        boolean oldValue = this.fitPage;
        this.fitPage = fitPage;

        if (updateScaleAndTranslate()) {
            repaintBuffer = true;
            updateFinder(false);
        }

        firePropertyChange("fitPage", oldValue, fitPage);
    }

    /**
     * Gets graph component.
     *
     * @return the graph component
     */
    public mxGraphComponent getGraphComponent() {
        return graphComponent;
    }

    /**
     * Fires a property change event for <code>graphComponent</code>.
     *
     * @param graphComponent the graphComponent to set
     */
    public void setGraphComponent(mxGraphComponent graphComponent) {
        mxGraphComponent oldValue = this.graphComponent;

        if (this.graphComponent != null) {
            this.graphComponent.getGraph().removeListener(repaintHandler);
            this.graphComponent.getGraphControl().removeComponentListener(
                    componentHandler);
            this.graphComponent.getHorizontalScrollBar()
                    .removeAdjustmentListener(adjustmentHandler);
            this.graphComponent.getVerticalScrollBar()
                    .removeAdjustmentListener(adjustmentHandler);
        }

        this.graphComponent = graphComponent;

        if (this.graphComponent != null) {
            this.graphComponent.getGraph().addListener(mxEvent.REPAINT,
                    repaintHandler);
            this.graphComponent.getGraphControl().addComponentListener(
                    componentHandler);
            this.graphComponent.getHorizontalScrollBar().addAdjustmentListener(
                    adjustmentHandler);
            this.graphComponent.getVerticalScrollBar().addAdjustmentListener(
                    adjustmentHandler);
        }

        if (updateScaleAndTranslate()) {
            repaintBuffer = true;
            repaint();
        }

        firePropertyChange("graphComponent", oldValue, graphComponent);
    }

    /**
     * Checks if the triple buffer exists and creates a new one if
     * it does not. Also compares the size of the buffer with the
     * size of the graph and drops the buffer if it has a
     * different size.
     */
    public void checkTripleBuffer() {
        if (tripleBuffer != null) {
            if (tripleBuffer.getWidth() != getWidth()
                    || tripleBuffer.getHeight() != getHeight()) {
                // Resizes the buffer (destroys existing and creates new)
                destroyTripleBuffer();
            }
        }

        if (tripleBuffer == null) {
            createTripleBuffer(getWidth(), getHeight());
        }
    }

    /**
     * Creates the tripleBufferGraphics and tripleBuffer for the given
     * dimension and draws the complete graph onto the triplebuffer.
     *
     * @param width  the width
     * @param height the height
     */
    protected void createTripleBuffer(int width, int height) {
        try {
            tripleBuffer = mxUtils.createBufferedImage(width, height, null);
            tripleBufferGraphics = tripleBuffer.createGraphics();

            // Repaints the complete buffer
            repaintTripleBuffer(null);
        } catch (OutOfMemoryError error) {
            log.log(Level.SEVERE, "Failed to create a triple buffer", error);
        }
    }

    /**
     * Destroys the tripleBuffer and tripleBufferGraphics objects.
     */
    public void destroyTripleBuffer() {
        if (tripleBuffer != null) {
            tripleBuffer = null;
            tripleBufferGraphics.dispose();
            tripleBufferGraphics = null;
        }
    }

    /**
     * Clears and repaints the triple buffer at the given rectangle or repaints
     * the complete buffer if no rectangle is specified.
     *
     * @param clip the clip
     */
    public void repaintTripleBuffer(Rectangle clip) {
        if (tripleBuffered && tripleBufferGraphics != null) {
            if (clip == null) {
                clip = new Rectangle(tripleBuffer.getWidth(),
                        tripleBuffer.getHeight());
            }

            // Clears and repaints the dirty rectangle using the
            // graphics canvas of the graph component as a renderer
            mxUtils.clearRect(tripleBufferGraphics, clip, null);
            tripleBufferGraphics.setClip(clip);
            paintGraph(tripleBufferGraphics);
            tripleBufferGraphics.setClip(null);

            repaintBuffer = false;
            repaintClip = null;
        }
    }

    /**
     * Update finder.
     *
     * @param repaint the repaint
     */
    public void updateFinder(boolean repaint) {
        Rectangle rect = graphComponent.getViewport().getViewRect();

        int x = (int) Math.round(rect.x * scale);
        int y = (int) Math.round(rect.y * scale);
        int w = (int) Math.round((rect.x + rect.width) * scale) - x;
        int h = (int) Math.round((rect.y + rect.height) * scale) - y;

        updateFinderBounds(new Rectangle(x + translate.x, y + translate.y,
                w + 1, h + 1), repaint);
    }

    /**
     * Update finder bounds.
     *
     * @param bounds  the bounds
     * @param repaint the repaint
     */
    public void updateFinderBounds(Rectangle bounds, boolean repaint) {
        if (bounds != null && !bounds.equals(finderBounds)) {
            Rectangle old = new Rectangle(finderBounds);
            finderBounds = bounds;

            // LATER: Fix repaint region to be smaller
            if (repaint) {
                old = old.union(finderBounds);
                old.grow(3, 3);
                repaint(old);
            }
        }
    }

    /**
     *
     */
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        paintBackground(g);

        if (graphComponent != null) {
            // Creates or destroys the triple buffer as needed
            if (tripleBuffered) {
                checkTripleBuffer();
            } else if (tripleBuffer != null) {
                destroyTripleBuffer();
            }

            // Updates the dirty region from the buffered graph image
            if (tripleBuffer != null) {
                if (repaintBuffer) {
                    repaintTripleBuffer(null);
                } else if (repaintClip != null) {
                    repaintClip.grow(1 / scale);

                    repaintClip.setX(repaintClip.getX() * scale + translate.x);
                    repaintClip.setY(repaintClip.getY() * scale + translate.y);
                    repaintClip.setWidth(repaintClip.getWidth() * scale);
                    repaintClip.setHeight(repaintClip.getHeight() * scale);

                    repaintTripleBuffer(repaintClip.getRectangle());
                }

                mxUtils.drawImageClip(g, tripleBuffer, this);
            }

            // Paints the graph directly onto the graphics
            else {
                paintGraph(g);
            }

            paintForeground(g);
        }
    }

    /**
     * Paints the background.
     *
     * @param g the g
     */
    protected void paintBackground(Graphics g) {
        if (graphComponent != null) {
            Graphics2D g2 = (Graphics2D) g;
            AffineTransform tx = g2.getTransform();

            try {
                // Draws the background of the outline if a graph exists
                g.setColor(graphComponent.getPageBackgroundColor());
                mxUtils.fillClippedRect(g, 0, 0, getWidth(), getHeight());

                g2.translate(translate.x, translate.y);
                g2.scale(scale, scale);

                // Draws the scaled page background
                if (!graphComponent.isPageVisible()) {
                    Color bg = graphComponent.getBackground();

                    if (graphComponent.getViewport().isOpaque()) {
                        bg = graphComponent.getViewport().getBackground();
                    }

                    g.setColor(bg);
                    Dimension size = graphComponent.getGraphControl().getSize();

                    // Paints the background of the drawing surface
                    mxUtils.fillClippedRect(g, 0, 0, size.width, size.height);
                    g.setColor(g.getColor().darker().darker());
                    g.drawRect(0, 0, size.width, size.height);
                } else {
                    // Paints the page background using the graphics scaling
                    graphComponent.paintBackgroundPage(g);
                }
            } finally {
                g2.setTransform(tx);
            }
        } else {
            // Draws the background of the outline if no graph exists
            g.setColor(getBackground());
            mxUtils.fillClippedRect(g, 0, 0, getWidth(), getHeight());
        }
    }

    /**
     * Paints the graph outline.
     *
     * @param g the g
     */
    public void paintGraph(Graphics g) {
        if (graphComponent != null) {
            Graphics2D g2 = (Graphics2D) g;
            AffineTransform tx = g2.getTransform();

            try {
                Point tr = graphComponent.getGraphControl().getTranslate();
                g2.translate(translate.x + tr.getX() * scale,
                        translate.y + tr.getY() * scale);
                g2.scale(scale, scale);

                // Draws the scaled graph
                graphComponent.getGraphControl().drawGraph(g2, drawLabels);
            } finally {
                g2.setTransform(tx);
            }
        }
    }

    /**
     * Paints the foreground. Foreground is dynamic and should never be made
     * part of the triple buffer. It is painted on top of the buffer.
     *
     * @param g the g
     */
    protected void paintForeground(Graphics g) {
        if (graphComponent != null) {
            Graphics2D g2 = (Graphics2D) g;

            Stroke stroke = g2.getStroke();
            g.setColor(Color.BLUE);
            g2.setStroke(new BasicStroke(3));
            g.drawRect(finderBounds.x, finderBounds.y, finderBounds.width,
                    finderBounds.height);

            if (zoomHandleVisible) {
                g2.setStroke(stroke);
                g.setColor(DEFAULT_ZOOMHANDLE_FILL);
                g.fillRect(finderBounds.x + finderBounds.width - 6, finderBounds.y
                        + finderBounds.height - 6, 8, 8);
                g.setColor(Color.BLACK);
                g.drawRect(finderBounds.x + finderBounds.width - 6, finderBounds.y
                        + finderBounds.height - 6, 8, 8);
            }
        }
    }

    /**
     * Returns true if the scale or translate has changed.
     *
     * @return the boolean
     */
    public boolean updateScaleAndTranslate() {
        double newScale = 1;
        int dx = 0;
        int dy = 0;

        if (this.graphComponent != null) {
            Dimension graphSize = graphComponent.getGraphControl().getSize();
            Dimension outlineSize = getSize();

            int gw = (int) graphSize.getWidth();
            int gh = (int) graphSize.getHeight();

            if (gw > 0 && gh > 0) {
                boolean magnifyPage = graphComponent.isPageVisible()
                        && isFitPage()
                        && graphComponent.getHorizontalScrollBar().isVisible()
                        && graphComponent.getVerticalScrollBar().isVisible();
                double graphScale = graphComponent.getGraph().getView()
                        .getScale();
                mxPoint trans = graphComponent.getGraph().getView()
                        .getTranslate();

                int w = (int) outlineSize.getWidth() - 2 * outlineBorder;
                int h = (int) outlineSize.getHeight() - 2 * outlineBorder;

                if (magnifyPage) {
                    gw -= 2 * Math.round(trans.getX() * graphScale);
                    gh -= 2 * Math.round(trans.getY() * graphScale);
                }

                newScale = Math.min((double) w / gw, (double) h / gh);

                dx += (int) Math
                        .round((outlineSize.getWidth() - gw * newScale) / 2);
                dy += (int) Math
                        .round((outlineSize.getHeight() - gh * newScale) / 2);

                if (magnifyPage) {
                    dx -= Math.round(trans.getX() * newScale * graphScale);
                    dy -= Math.round(trans.getY() * newScale * graphScale);
                }
            }
        }

        if (newScale != scale || translate.x != dx || translate.y != dy) {
            scale = newScale;
            translate.setLocation(dx, dy);

            return true;
        } else {
            return false;
        }
    }

    /**
     * The type Mouse tracker.
     */
    public class MouseTracker implements MouseListener, MouseMotionListener {
        /**
         * The Start.
         */
        protected Point start = null;

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseListener#mousePressed(java.awt.event.MouseEvent)
         */
        public void mousePressed(MouseEvent e) {
            zoomGesture = hitZoomHandle(e.getX(), e.getY());

            if (graphComponent != null && !e.isConsumed()
                    && !e.isPopupTrigger()
                    && (finderBounds.contains(e.getPoint()) || zoomGesture)) {
                start = e.getPoint();
            }
        }

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseMotionListener#mouseDragged(java.awt.event.MouseEvent)
         */
        public void mouseDragged(MouseEvent e) {
            if (isEnabled() && start != null) {
                if (zoomGesture) {
                    Rectangle bounds = graphComponent.getViewport()
                            .getViewRect();
                    double viewRatio = bounds.getWidth() / bounds.getHeight();

                    bounds = new Rectangle(finderBounds);
                    bounds.width = (int) Math
                            .max(0, (e.getX() - bounds.getX()));
                    bounds.height = (int) Math.max(0,
                            (bounds.getWidth() / viewRatio));

                    updateFinderBounds(bounds, true);
                } else {
                    // TODO: To enable constrained moving, that is, moving
                    // into only x- or y-direction when shift is pressed,
                    // we need the location of the first mouse event, since
                    // the movement can not be constrained for incremental
                    // steps as used below.
                    int dx = (int) ((e.getX() - start.getX()) / scale);
                    int dy = (int) ((e.getY() - start.getY()) / scale);

                    // Keeps current location as start for delta movement
                    // of the scrollbars
                    start = e.getPoint();

                    graphComponent.getHorizontalScrollBar().setValue(
                            graphComponent.getHorizontalScrollBar().getValue()
                                    + dx);
                    graphComponent.getVerticalScrollBar().setValue(
                            graphComponent.getVerticalScrollBar().getValue()
                                    + dy);
                }
            }
        }

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseListener#mouseReleased(java.awt.event.MouseEvent)
         */
        public void mouseReleased(MouseEvent e) {
            if (start != null) {
                if (zoomGesture) {
                    double dx = e.getX() - start.getX();
                    double w = finderBounds.getWidth();

                    final JScrollBar hs = graphComponent
                            .getHorizontalScrollBar();
                    final double sx;

                    if (hs != null) {
                        sx = (double) hs.getValue() / hs.getMaximum();
                    } else {
                        sx = 0;
                    }

                    final JScrollBar vs = graphComponent.getVerticalScrollBar();
                    final double sy;

                    if (vs != null) {
                        sy = (double) vs.getValue() / vs.getMaximum();
                    } else {
                        sy = 0;
                    }

                    mxGraphView view = graphComponent.getGraph().getView();
                    double scale = view.getScale();
                    double newScale = scale - (dx * scale) / w;
                    double factor = newScale / scale;
                    view.setScale(newScale);

                    if (hs != null) {
                        hs.setValue((int) (sx * hs.getMaximum() * factor));
                    }

                    if (vs != null) {
                        vs.setValue((int) (sy * vs.getMaximum() * factor));
                    }
                }

                zoomGesture = false;
                start = null;
            }
        }

        /**
         * Hit zoom handle boolean.
         *
         * @param x the x
         * @param y the y
         * @return the boolean
         */
        public boolean hitZoomHandle(int x, int y) {
            return new Rectangle(finderBounds.x + finderBounds.width - 6,
                    finderBounds.y + finderBounds.height - 6, 8, 8).contains(x,
                    y);
        }

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseMotionListener#mouseMoved(java.awt.event.MouseEvent)
         */
        public void mouseMoved(MouseEvent e) {
            if (hitZoomHandle(e.getX(), e.getY())) {
                setCursor(new Cursor(Cursor.HAND_CURSOR));
            } else if (finderBounds.contains(e.getPoint())) {
                setCursor(new Cursor(Cursor.MOVE_CURSOR));
            } else {
                setCursor(new Cursor(Cursor.DEFAULT_CURSOR));
            }
        }

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseListener#mouseClicked(java.awt.event.MouseEvent)
         */
        public void mouseClicked(MouseEvent e) {
            // ignore
        }

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseListener#mouseEntered(java.awt.event.MouseEvent)
         */
        public void mouseEntered(MouseEvent e) {
            // ignore
        }

        /*
         * (non-Javadoc)
         * @see java.awt.event.MouseListener#mouseExited(java.awt.event.MouseEvent)
         */
        public void mouseExited(MouseEvent e) {
            // ignore
        }

    }

}
